package eostre

// todo make gott

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"strings"

	"apiote.xyz/p/asgard/idavollr"
	"apiote.xyz/p/asgard/jotunheim"

	"apiote.xyz/p/gott/v2"

	"github.com/ProtonMail/gopenpgp/v2/helper"
	"github.com/bytesparadise/libasciidoc"
	"github.com/bytesparadise/libasciidoc/pkg/configuration"
	"github.com/emersion/go-imap"
	"github.com/emersion/go-message"
	_ "github.com/emersion/go-message/charset"
)

type UnauthorisedSenderError struct {
	sender string
}

func (e UnauthorisedSenderError) Error() string {
	return "message from unauthorised sender " + e.sender
}

type EostreMailbox struct {
	idavollr.Mailbox
	delSeqset    *imap.SeqSet
	doneMessages int
}

type EostreImapMessage struct {
	idavollr.ImapMessage
	delSeqset  *imap.SeqSet
	config     jotunheim.Config
	mime       string
	mimeParams map[string]string
	part       *message.Entity
	subject    string
	partBody   []byte
	filename   string
	asciidoc   string
	writer     *bytes.Buffer
	html       string
	file       *os.File
}

func Eostre(config jotunheim.Config) (int, error) {
	mailbox := &EostreMailbox{
		Mailbox: idavollr.Mailbox{
			MboxName: "INBOX",
			ImapAdr:  config.Eostre.ImapAddress,
			ImapUser: config.Eostre.ImapUsername,
			ImapPass: config.Eostre.ImapPassword,
			Conf:     config,
		},
		delSeqset: new(imap.SeqSet),
	}
	mailbox.SetupChannels()
	r := gott.R[idavollr.AbstractMailbox]{
		S: mailbox,
	}.
		Bind(idavollr.Connect).
		Tee(idavollr.Login).
		Bind(idavollr.SelectInbox).
		Tee(idavollr.CheckEmptyBox).
		Map(idavollr.FetchMessages).
		Bind(downloadEntries).
		Tee(deleteMessages).
		Tee(idavollr.Expunge)

	return r.S.(*EostreMailbox).doneMessages, r.E
}

func downloadEntries(m idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
	em := m.(*EostreMailbox)
	for msg := range em.Messages() {
		imapMessage := idavollr.ImapMessage{
			Msg:      msg,
			Sect:     m.Section(),
			Mimetype: "",
		}
		imapMessage.SetClient(m.Client())
		r := gott.R[idavollr.AbstractImapMessage]{
			S: &EostreImapMessage{
				ImapMessage: imapMessage,
				config:      m.Config(),
				delSeqset:   em.delSeqset,
			},
		}.
			Tee(checkSender).
			Bind(idavollr.ReadMessageBody).
			Bind(idavollr.ParseMimeMessage).
			Bind(getContentType).
			Map(getPlainPart).
			Bind(getEncryptedPart).
			Tee(checkSelectedPart).
			Map(getSubject).
			Bind(readPartBody).
			Bind(prepareAsciidoc).
			Bind(convertAsciidoc).
			Map(cleanHtml).
			Bind(openFile).
			Tee(writeFile).
			Recover(closeFile).
			SafeTee(markDeleteMessage).
			Recover(ignoreUnauthorisedSender).
			Recover(idavollr.RecoverMalformedMessage)

		if r.E != nil {
			log.Printf("message %s (%s) errored: %s\n", r.S.(*EostreImapMessage).subject, r.S.Message().Uid, r.E.Error())
		} else {
			em.doneMessages++
		}
	}
	return em, nil
}

func checkSender(m idavollr.AbstractImapMessage) error {
	em := m.(*EostreImapMessage)
	sender := m.Message().Envelope.From[0]
	if sender.Address() != em.config.Eostre.AuthorisedSender {
		return UnauthorisedSenderError{sender: sender.Address()}
	}
	return nil
}

func getContentType(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	t, params, err := m.MimeMessage().Header.ContentType()
	em.mime = t
	em.mimeParams = params
	return em, err
}

func getPlainPart(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
	em := m.(*EostreImapMessage)
	if em.mime == "text/plain" {
		em.part = m.MimeMessage()
	}
	return em
}

// TODO break up
func getEncryptedPart(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	if em.mime == "multipart/encrypted" && em.mimeParams["protocol"] == "application/pgp-encrypted" {
		mr := m.MimeMessage().MultipartReader()
		for {
			p, err := mr.NextPart()
			if err == io.EOF {
				break
			} else if err != nil {
				return em, fmt.Errorf("while reading next part: %w", err)
			}

			t, _, err := p.Header.ContentType()
			if err != nil {
				return em, fmt.Errorf("while getting content type: %w", err)
			}

			if t == "application/octet-stream" {
				bodyReader := p.Body
				body, err := io.ReadAll(bodyReader)
				if err != nil {
					return em, fmt.Errorf("while reading body: %w", err)
				}
				decrypted, err := helper.DecryptVerifyMessageArmored(em.config.Eostre.PublicKey, em.config.Eostre.PrivateKey, []byte(em.config.Eostre.PrivateKeyPass), string(body))
				if err != nil {
					return em, fmt.Errorf("while decrypting body: %w", err)
				}
				em.part, err = message.Read(strings.NewReader(decrypted))
				return em, err
			}
		}
	}
	return em, nil
}

func checkSelectedPart(m idavollr.AbstractImapMessage) error {
	em := m.(*EostreImapMessage)
	if em.part == nil {
		return idavollr.MalformedMessageError{
			Cause:     errors.New("text/plain or multipart/encrypted  not found"),
			MessageID: m.Message().Envelope.MessageId,
		}
	}
	return nil
}

func getSubject(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
	em := m.(*EostreImapMessage)
	em.subject = em.Message().Envelope.Subject
	encryptedSubject := em.part.Header.Get("Subject")
	if encryptedSubject != "" {
		em.subject = encryptedSubject
	}
	return em
}

func readPartBody(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	body, err := io.ReadAll(em.part.Body)
	em.partBody = body
	return em, err
}

func prepareAsciidoc(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	em.asciidoc, _, _ = strings.Cut(string(em.partBody), "\n-- ")

	em.filename = em.Message().Envelope.Date.Format("20060102.html")
	_, err := os.Stat(em.filename)
	if err == nil {
		em.asciidoc = "=== " + em.Message().Envelope.Date.Format("03:04 -0700") + "\n\n_" + em.subject + "_\n\n" + em.asciidoc
	} else {
		if errors.Is(err, os.ErrNotExist) {
			em.asciidoc = "== " + em.Message().Envelope.Date.Format("Jan 2") + "\n\n_" + em.subject + "_\n\n" + em.asciidoc
			err = nil
		}
	}
	return em, err
}

func convertAsciidoc(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	reader := strings.NewReader(em.asciidoc)
	em.writer = bytes.NewBuffer([]byte{})
	config := configuration.NewConfiguration()
	_, err := libasciidoc.Convert(reader, em.writer, config)
	return em, err
}

func cleanHtml(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
	em := m.(*EostreImapMessage)
	em.html = string(em.writer.Bytes())
	em.html = strings.ReplaceAll(em.html, "<div class=\"sect2\">\n", "")
	em.html = strings.ReplaceAll(em.html, "<div class=\"sect1\">\n", "")
	em.html = strings.ReplaceAll(em.html, "<div class=\"sectionbody\">\n", "")
	em.html = strings.ReplaceAll(em.html, "<div class=\"paragraph\">\n", "")
	em.html = strings.ReplaceAll(em.html, "</div>\n", "")
	return em
}

func openFile(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	f, err := os.OpenFile(em.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	em.file = f
	return em, err
}

func writeFile(m idavollr.AbstractImapMessage) error {
	em := m.(*EostreImapMessage)
	_, err := em.file.WriteString(em.html)
	return err
}

func closeFile(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	if em.file != nil {
		em.file.Close()
	}
	return m, err
}

func markDeleteMessage(m idavollr.AbstractImapMessage) {
	em := m.(*EostreImapMessage)
	em.delSeqset.AddNum(em.Message().Uid)
}

func ignoreUnauthorisedSender(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
	em := m.(*EostreImapMessage)
	var unauthorisedSenderError UnauthorisedSenderError
	if errors.As(err, &unauthorisedSenderError) {
		em.delSeqset.AddNum(em.Message().Uid)
		log.Printf("ignoring from %s as not authorised\n", unauthorisedSenderError.sender)
		return em, nil
	}
	return em, err
}

func deleteMessages(m idavollr.AbstractMailbox) error {
	em := m.(*EostreMailbox)
	if !em.delSeqset.Empty() {
		item := imap.FormatFlagsOp(imap.AddFlags, true)
		flags := []interface{}{imap.DeletedFlag}
		err := em.Client().UidStore(em.delSeqset, item, flags, nil)
		return err
	}
	return nil
}
